using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Threading;
using System.Xml;

using Banshee.Base;
using Banshee.Widgets;
using Banshee.Sources;
using Banshee.SmartPlaylist;

using Gtk;

namespace Banshee.Plugins.iTunesImporter
{    
    public static class iTunesImporter
    {
        private static readonly Gdk.Pixbuf icon = IconThemeUtils.LoadIcon(22, "gtk-go-down");
        private static ActiveUserEvent user_event;
        private static string XmlUri = null;
        private static bool getRatings, getStats, getPlaylists, getSPlaylists;
        private static uint _total, _ratings, _stats, _playlists, _splaylists;
        private static List<SmartPlaylist> partial, failed;
        private static Thread thread;

        public static void OnImportManagerImportRequested(object o, ImportEventArgs args)
        {
            if(XmlUri == null && args.FileName.IndexOf("iTunes Music Library.xml") != -1) {
                XmlUri = args.FileName;
            }
        }

        internal static void OnImportManagerFinished (object o, EventArgs args)
        {
            if (XmlUri != null && (thread == null || !thread.IsAlive)) {
                Import();
            }
        }

        internal static void Import()
        {
            if(thread != null && thread.IsAlive) {
                Console.WriteLine("Another iTunes import is already running");
                return;
            }
            
            // Make sure all of our DB columns are present and well
            try {
                Globals.Library.Db.Execute("SELECT ItunesSynced FROM Tracks");
            } catch(Exception e) {
                if (e.Message != "no such column: ItunesSynced") {
                    throw e;
                }
                Globals.Library.Db.Execute("ALTER TABLE Tracks ADD ItunesSynced INTEGER");
                Globals.Library.Db.Execute("UPDATE Tracks SET ItunesSynced = 0");
            }
            try {
                Globals.Library.Db.Execute("SELECT ItunesID FROM Tracks");
            } catch(Exception e) {
                if (e.Message != "no such column: ItunesID") {
                    throw e;
                }
                Globals.Library.Db.Execute("ALTER TABLE Tracks ADD ItunesID INTEGER");
            } finally {
                Globals.Library.Db.Execute("UPDATE Tracks SET ItunesID = 0");
            }
            
            _total = 0;
            _ratings = 0;
            _stats = 0;
            _playlists = 0;
            _splaylists = 0;
            partial = new List<SmartPlaylist>();
            failed = new List<SmartPlaylist>();

            using(iTunesSettings settings = new iTunesSettings()) {
                if(settings.Run() == 0) {
                    settings.Destroy();
                    XmlUri = null;
                    return;
                } else {
                    getRatings = settings.Ratings;
                    getStats = settings.Stats;
                    getPlaylists = settings.Playlists;
                    getSPlaylists = settings.SPlaylists;
                }
                settings.Destroy();
            }

            user_event = new ActiveUserEvent("Import from iTunes");
            user_event.Header = "Importing iTunes data";
            user_event.Message = "Loading iTunes Library...";
            user_event.Icon = icon;
            user_event.CancelRequested += OnCancelRequested;
            
            thread = ThreadAssist.Spawn(ImportSongData);
        }

        public static void ImportSongData()
        {
            XmlDocument x = new XmlDocument();
            x.Load(XmlUri);
            XmlNode TracksDict = x.FirstChild.NextSibling.NextSibling.FirstChild.FirstChild;
            while(TracksDict.Name != "dict") {
                TracksDict = TracksDict.NextSibling;
            }
            foreach(XmlNode dict in TracksDict.ChildNodes) {
                if(dict.Name == "dict") {
                    ProcessSong(dict.ChildNodes);
                }
            }
            if(getPlaylists || getSPlaylists) {
                XmlNode playlistArray = TracksDict.NextSibling.NextSibling;
                XmlNodeList playlistDicts = playlistArray.ChildNodes;
                for(int i = 1; i < playlistDicts.Count; i++) {
                    ProcessPlaylist(playlistDicts[i]);
                }
            }
            Done();
        }
        
        private static void OnCancelRequested(object o, EventArgs e)
        {
            Done(true);
            Destroy();
        }

        private static void Done()
        {
            Done(false);
        }
        
        private static void Done(bool interrupt)
        {
            user_event.Dispose();
            user_event = null;

            SummarySource resultSource = new SummarySource(
                _ratings,
                _stats,
                _playlists,
                _splaylists,
                partial,
                failed,
                interrupt);
            
            LibrarySource.Instance.AddChildSource(resultSource);
            SourceManager.SetActiveSource(resultSource);
            
            partial = null;
            failed = null;
            XmlUri = null;
        }

        public static void Destroy()
        {
            if(user_event != null) {
                user_event.Dispose();
                user_event = null;
            }
            if(thread != null && thread.IsAlive) {
                thread.Abort();
                thread = null;
            }
        }
        
        private static void ProcessSong(XmlNodeList keys)
        {
            string location = null;
            int iTunesID = 0;
            byte rating = 0;
            int playCount = 0;
            DateTime lastPlayed = new DateTime();
            bool match = false;

            foreach(XmlNode key in keys) {
                if(key.Name != "key") {
                    continue;
                }
                switch (key.InnerText) {
                case "Track ID":
                    iTunesID = Int32.Parse(key.NextSibling.InnerText);
                    break;
                case "Play Count":
                    playCount = Int16.Parse(key.NextSibling.InnerText);
                    match = true;
                    break;
                case "Play Date UTC":
                    lastPlayed = DateTime.Parse(key.NextSibling.InnerText);
                    match = true;
                    break;
                case "Rating":
                    rating = (byte)Int32.Parse(key.NextSibling.InnerText);
                    match = true;
                    break;
                case "Location":
					location = key.NextSibling.InnerText;
					break;
                }
            }

            if(!match || location == null) {
                return;
            }

            int trackID = FindTrackID(iTunesID, location);
            LibraryTrackInfo info;
            try {
                info = Globals.Library.GetTrack(trackID);
            } catch {
                return;
            }

            // Rating
            if(getRatings && rating != 0 && info.Rating == 0) {
                info.Rating = (uint)rating / 20;
                _ratings++;
            }
            bool statsUpdated = false;

            // Play count
            while(getStats && playCount != 0) {
                int previousSync;
                try {
                    previousSync = (int)Globals.Library.Db.QuerySingle(String.Format(
                        "SELECT ItunesSynced FROM Tracks WHERE TrackID = {0}", trackID));
                } catch {
                    previousSync = 0;
                }
                if(playCount <= previousSync) {
                    break;
                }
                Globals.Library.Db.Execute(String.Format(
                    "UPDATE Tracks SET ItunesSynced = {0} WHERE TrackID = {1}", playCount, trackID));
                playCount -= previousSync;
                info.PlayCount += (uint)playCount;
                _stats++;
                statsUpdated = true;
                break;
            }

            // Last played
            if(getStats && lastPlayed != new DateTime () && DateTime.Compare (lastPlayed, info.LastPlayed) > 0) {
                info.LastPlayed = lastPlayed;
                if(!statsUpdated) {
                    _stats++;
                }
            }

            // iTunesID
            Globals.Library.Db.Execute(String.Format(
                "UPDATE Tracks SET ItunesID = {0} WHERE TrackID = {1}", iTunesID, trackID));

            info.Save();
            _total++;
            user_event.Message = _total + " songs processed...";
            return;
		}

        private static int FindTrackID(int iTunesID, string location)
        // This is a pretty messy method
        {
            int trackID = 0;
            string uri = null;
            bool retry = true;
        AmpersandFallback:
            int lastSlashLoc = location.Length - 1;
            int resultCount = 0;
            
            // Since the mount point can differ, we start querying
            // for the filename and then move up the directory
            // structure until there is just one result
            while(resultCount != 1) {
                resultCount = 0;
                lastSlashLoc = location.LastIndexOf(@"/", lastSlashLoc - 1);
                if(lastSlashLoc == -1) {
                    return 0;
                }
                uri = location.Substring(lastSlashLoc + 1);
                try {
                    using(IDataReader reader = Globals.Library.Db.Query(String.Format(
                        "SELECT TrackID FROM Tracks WHERE Uri LIKE \"%{0}\"", uri))) {
                        while(reader.Read()) {
                            trackID = (int)reader [0];
                            resultCount++;
                        }
                    }
                } catch {
                    if(retry) {
                        location = location.Replace(
                            @"&", @"%26");
                        retry = false;
                        goto AmpersandFallback;
                    }
                }
                if(resultCount == 0) {
                    return 0;
                }
            }
            return trackID;
        }
        
        private static void ProcessPlaylist(XmlNode node)
        {
            string name = "";
            bool skip = false;
            byte[] smartInfo = null;
            byte[] smartCriteria = null;
            XmlNode array = null;

            XmlNodeList keys = node.ChildNodes;
            foreach(XmlNode key in keys) {
                if(key.Name=="key") {
                    switch(key.InnerText) {
                    case "Name":
                       name = key.NextSibling.InnerText;
                       //if(name == "Music Videos")
                           //skip = true;
                       break;
                    case "Smart Info":
                       smartInfo = Convert.FromBase64String(key.NextSibling.InnerText);
                       break;
                    case "Smart Criteria":
                        smartCriteria = Convert.FromBase64String(key.NextSibling.InnerText);
                        break;
                    default:
                        if (key.InnerText == "Audiobooks" ||
                            key.InnerText == "Music" ||
                            key.InnerText == "Movies" ||
                            key.InnerText == "Party Shuffle" ||
                            key.InnerText == "Podcasts" ||
                            key.InnerText == "Purchased Music" ||
                            key.InnerText == "TV Shows") {
                            skip = true;
                        }
                        break;
                    }
                    if(skip) {
                        break;
                    }
                } else if(key.Name == "array") {
                    array = key;
                }
            }
            if(skip) {
                return;
            } else if(getPlaylists && smartInfo == null) {
                ProcessRegularPlaylist(name, array);
            } else if(getSPlaylists && smartInfo != null) {
                ProcessSmartPlaylist(name, smartInfo, smartCriteria, array);
            }
        }

        private static void ProcessRegularPlaylist(string name, XmlNode array)
        {
            foreach(PlaylistSource playlist in PlaylistSource.Playlists) {
                if(playlist.Name == name) {
                    playlist.Unmap();
                    Console.WriteLine("Replacing playlist " + name);
                    break;
                }
            }
            PlaylistSource playlistSource = new PlaylistSource();
            playlistSource.Rename(name);
            LibrarySource.Instance.AddChildSource(playlistSource);

            user_event.Message = "Importing playlist "+name+"...";

            // Get the songs in the playlists
            if(array != null) {
                XmlNodeList tempDicts = array.ChildNodes;
                foreach(XmlNode dict in tempDicts) {
                    int itunesID = Int32.Parse(dict.ChildNodes[1].InnerText);
                    int trackID;
                    TrackInfo track;
                    try {
                        using(IDataReader reader = Globals.Library.Db.Query(String.Format(
                            "SELECT TrackID FROM Tracks WHERE ItunesID = {0}", itunesID))) {
                            reader.Read();
                            trackID = (int)reader[0];
                        }
                        track = Globals.Library.GetTrack(trackID);
					} catch {
                        continue;
					}
					playlistSource.AddTrack(track);
                }
            }
            playlistSource.Commit();
            _playlists++;
        }

        private static void ProcessSmartPlaylist(string name, byte[] info, byte[] criteria, XmlNode array)
        {
            user_event.Message = "Importing playlist "+name+"...";
            SmartPlaylist smartPlaylist = SmartPlaylistParser.Parse(info, criteria);
            smartPlaylist.Name = name;
            if(!(smartPlaylist.Query == "" && smartPlaylist.Ignore != "") || smartPlaylist.LimitNumber != 0) {
                // Is there a collection of Smart Playlists?
                ChildSource[] tempArray = new ChildSource[LibrarySource.Instance.Children.Count];
                LibrarySource.Instance.Children.CopyTo(tempArray, 0);
                for(int i = 0; i < tempArray.Length; i++) {
                    if(tempArray[i].Name == name && tempArray[i].GenericName == "Smart Playlist") {
                        tempArray[i].Unmap();
                        Console.WriteLine("Replacing smart playlist " + name);
                        break;
                    }
                }
                SmartPlaylistSource smartPlaylistSource = new SmartPlaylistSource(
                    name,
                    (smartPlaylist.Query == "") ? null : " " + smartPlaylist.Query,
                    smartPlaylist.OrderBy,
                    smartPlaylist.LimitNumber.ToString(),
                    smartPlaylist.LimitMethod
                );
                if(!SourceManager.ContainsSource(smartPlaylistSource) &&
                    SourceManager.ContainsSource(LibrarySource.Instance)) {
                    LibrarySource.Instance.AddChildSource(smartPlaylistSource);
                }
            }
            if (smartPlaylist.Ignore != "") {
                if (smartPlaylist.Query != "") {
                    partial.Add(smartPlaylist);
                } else {
                    failed.Add(smartPlaylist);
                }
                ProcessRegularPlaylist(name, array);
            } else {
                _splaylists++;
            }
        }
    }
}
